/**
* Copyright (C) 2019-2021 Xilinx, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may
* not use this file except in compliance with the License. A copy of the
* License is located at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

#include <xcl2.hpp>

#include <cstdio>
#include <fstream>
#include <iosfwd>
#include <string>
#include <vector>

using std::ifstream;
using std::ios;
using std::streamsize;
using std::string;
using std::vector;

#define vendor_name "Xilinx"

#define ERROR_CASE(err) \
    case err:           \
        return #err;    \
        break

const char* error_string(cl_int error_code) {
    switch (error_code) {
        ERROR_CASE(CL_SUCCESS);
        ERROR_CASE(CL_DEVICE_NOT_FOUND);
        ERROR_CASE(CL_DEVICE_NOT_AVAILABLE);
        ERROR_CASE(CL_COMPILER_NOT_AVAILABLE);
        ERROR_CASE(CL_MEM_OBJECT_ALLOCATION_FAILURE);
        ERROR_CASE(CL_OUT_OF_RESOURCES);
        ERROR_CASE(CL_OUT_OF_HOST_MEMORY);
        ERROR_CASE(CL_PROFILING_INFO_NOT_AVAILABLE);
        ERROR_CASE(CL_MEM_COPY_OVERLAP);
        ERROR_CASE(CL_IMAGE_FORMAT_MISMATCH);
        ERROR_CASE(CL_IMAGE_FORMAT_NOT_SUPPORTED);
        ERROR_CASE(CL_BUILD_PROGRAM_FAILURE);
        ERROR_CASE(CL_MAP_FAILURE);
        ERROR_CASE(CL_INVALID_VALUE);
        ERROR_CASE(CL_INVALID_DEVICE_TYPE);
        ERROR_CASE(CL_INVALID_PLATFORM);
        ERROR_CASE(CL_INVALID_DEVICE);
        ERROR_CASE(CL_INVALID_CONTEXT);
        ERROR_CASE(CL_INVALID_QUEUE_PROPERTIES);
        ERROR_CASE(CL_INVALID_COMMAND_QUEUE);
        ERROR_CASE(CL_INVALID_HOST_PTR);
        ERROR_CASE(CL_INVALID_MEM_OBJECT);
        ERROR_CASE(CL_INVALID_IMAGE_FORMAT_DESCRIPTOR);
        ERROR_CASE(CL_INVALID_IMAGE_SIZE);
        ERROR_CASE(CL_INVALID_SAMPLER);
        ERROR_CASE(CL_INVALID_BINARY);
        ERROR_CASE(CL_INVALID_BUILD_OPTIONS);
        ERROR_CASE(CL_INVALID_PROGRAM);
        ERROR_CASE(CL_INVALID_PROGRAM_EXECUTABLE);
        ERROR_CASE(CL_INVALID_KERNEL_NAME);
        ERROR_CASE(CL_INVALID_KERNEL_DEFINITION);
        ERROR_CASE(CL_INVALID_KERNEL);
        ERROR_CASE(CL_INVALID_ARG_INDEX);
        ERROR_CASE(CL_INVALID_ARG_VALUE);
        ERROR_CASE(CL_INVALID_ARG_SIZE);
        ERROR_CASE(CL_INVALID_KERNEL_ARGS);
        ERROR_CASE(CL_INVALID_WORK_DIMENSION);
        ERROR_CASE(CL_INVALID_WORK_GROUP_SIZE);
        ERROR_CASE(CL_INVALID_WORK_ITEM_SIZE);
        ERROR_CASE(CL_INVALID_GLOBAL_OFFSET);
        ERROR_CASE(CL_INVALID_EVENT_WAIT_LIST);
        ERROR_CASE(CL_INVALID_EVENT);
        ERROR_CASE(CL_INVALID_OPERATION);
        ERROR_CASE(CL_INVALID_GL_OBJECT);
        ERROR_CASE(CL_INVALID_BUFFER_SIZE);
        ERROR_CASE(CL_INVALID_MIP_LEVEL);
        ERROR_CASE(CL_INVALID_GLOBAL_WORK_SIZE);
        ERROR_CASE(CL_COMPILE_PROGRAM_FAILURE);
        ERROR_CASE(CL_LINKER_NOT_AVAILABLE);
        ERROR_CASE(CL_LINK_PROGRAM_FAILURE);
        ERROR_CASE(CL_DEVICE_PARTITION_FAILED);
        ERROR_CASE(CL_KERNEL_ARG_INFO_NOT_AVAILABLE);
        ERROR_CASE(CL_INVALID_PROPERTY);
        ERROR_CASE(CL_INVALID_IMAGE_DESCRIPTOR);
        ERROR_CASE(CL_INVALID_COMPILER_OPTIONS);
        ERROR_CASE(CL_INVALID_LINKER_OPTIONS);
        ERROR_CASE(CL_INVALID_DEVICE_PARTITION_COUNT);
        default:
            printf("Unknown OpenCL Error (%d)\n", error_code);
            break;
    }
    return nullptr;
}

// This example just prints out string description for given OpenCL error code.
int main(int argc, char** argv) {
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " <XCLBIN File>" << std::endl;
        return EXIT_FAILURE;
    }

    auto binaryFile = argv[1];

    static const int elements = 1024;
    vector<int, aligned_allocator<int> > A(elements, 32);
    vector<int, aligned_allocator<int> > B(elements, 10);
    vector<int, aligned_allocator<int> > C(elements);

    // Error handling in OpenCL is performed using the cl_int specifier. OpenCL
    // functions either return or accept pointers to cl_int types to indicate if
    // an error occurred.
    cl_int err;
    cl::Context context;
    cl::CommandQueue q;
    cl::Kernel kernel;

    // OPENCL HOST CODE AREA ENDS
    std::vector<cl::Platform> platforms;
    OCL_CHECK(err, err = cl::Platform::get(&platforms));
    auto num_platforms = platforms.size();
    if (num_platforms == 0) {
        std::cout << "No platforms were found. This could be caused because the "
                     "OpenCL "
                     "icd was not installed in the /etc/OpenCL/vendors directory."
                  << std::endl;
        std::cout << "TEST FAILED" << std::endl;
        exit(EXIT_FAILURE);
    }

    std::string platformName;
    cl::Platform platform;
    size_t i = 0;
    for (i = 0; i < platforms.size(); i++) {
        platform = platforms[i];
        OCL_CHECK(err, platformName = platform.getInfo<CL_PLATFORM_NAME>(&err));
        if (platformName == vendor_name) {
            std::cout << "Found Platform" << std::endl;
            std::cout << "Platform Name: " << platformName.c_str() << std::endl;
            break;
        }
    }
    if (i == platforms.size()) {
        std::cout << "Error: Failed to find Xilinx platform" << std::endl;
        exit(EXIT_FAILURE);
    }

    std::vector<cl::Device> devices;
    if ((err = platform.getDevices(CL_DEVICE_TYPE_CPU, &devices))) {
        std::cout << "\nRecoverable Error calling cl::Platform::getDevices(): " << error_string(err) << std::endl
                  << "This error appears when we try to create a device and no "
                     "devices "
                     "are found on the platform. In this case we passed "
                     "CL_DEVICE_TYPE_CPU as the device type which is not available "
                     "on "
                     "the"
                  << platformName.c_str() << "platform. We intentionally threw this error so we will not "
                                             "be exiting the program.\n"
                  << std::endl;
    }

    // Getting ACCELERATOR Devices and selecting 1st such device
    OCL_CHECK(err, err = platform.getDevices(CL_DEVICE_TYPE_ALL, &devices));

    cl_context_properties props[3] = {CL_CONTEXT_PLATFORM, (cl_context_properties)(platforms[0])(), 0};

    // read_binary_file() is a utility API which will load the binaryFile
    // and will return pointer to file buffer.
    auto fileBuf = xcl::read_binary_file(binaryFile);
    cl::Program::Binaries bins{{fileBuf.data(), fileBuf.size()}};
    cl::Program::Binaries invalid_bin;

    bool valid_device = false;
    for (unsigned int i = 0; i < devices.size(); i++) {
        auto device = devices[i];

        // cl::Context::Context function call returns the object so they return error
        // codes
        // using pointers to cl_int as their last parameters
        {
            cl::Context context(0, props, nullptr, nullptr, &err);
            if (err) {
                std::cout << "Recoverable Error calling cl::Context::Context(): " << error_string(err) << std::endl
                          << "\tMost cl::Context* calls accept error codes as "
                             "their last parameter "
                             "instead of returning the error value. This error "
                             "occurred because "
                             "we passed zero(0) for the devices variable. We "
                             "intentionally threw "
                             "this error so we will not be exiting the program.\n"
                          << std::endl;
            }
        }
        // Creating Context and Command Queue for selected Device
        OCL_CHECK(err, context = cl::Context(device, nullptr, nullptr, nullptr, &err));
        OCL_CHECK(err, q = cl::CommandQueue(context, device, CL_QUEUE_PROFILING_ENABLE, &err));

        {
            cl::Program program(context, devices, invalid_bin, nullptr, &err);
            if (err) {
                std::cout << "\nRecoverable Error calling cl::Program::Program(): " << error_string(err) << std::endl
                          << "\tMost cl::Program* calls accept error codes as "
                             "their last parameter "
                             "instead of returning the error value. This error "
                             "occurred because "
                             "we passed an invalid_bin for the bins variable. We "
                             "intentionally threw "
                             "this error so we will not be exiting the program.\n"
                          << std::endl;
            }
        }
        std::cout << "Trying to program device[" << i << "]: " << device.getInfo<CL_DEVICE_NAME>() << std::endl;
        cl::Program program(context, {device}, bins, nullptr, &err);
        if (err != CL_SUCCESS) {
            std::cout << "Failed to program device[" << i << "] with xclbin file!\n";
        } else {
            {
                cl::Kernel kernel(program, "InvalidKernelName", &err);
                if (err) {
                    std::cout << "Recoverable Error calling cl::Kernel::Kernel(): " << error_string(err) << std::endl
                              << "Errors calling cl::Kernel are usually caused if the name "
                                 "passed into the function does not match a kernel in the "
                                 "binary. "
                                 "We intentionally caused this error so we will not be "
                                 "exiting the "
                                 "program.\n"
                              << std::endl;
                }
            }
            std::cout << "Device[" << i << "]: program successful!\n";
            OCL_CHECK(err, kernel = cl::Kernel(program, "vector_add", &err));
            valid_device = true;
            break; // we break because we found a valid device
        }
    }
    if (!valid_device) {
        std::cout << "Failed to program any device found, exit!\n";
        exit(EXIT_FAILURE);
    }

    // Allocate Buffer in Global Memory
    // Buffers are allocated using CL_MEM_USE_HOST_PTR for efficient memory and
    // Device-to-host communication
    {
        cl::Buffer buffer_a(context, CL_MEM_READ_ONLY, (size_t)0, nullptr, &err);
        if (err) {
            std::cout << "Recoverable Error calling clCreateBuffer: " << error_string(err) << std::endl
                      << "There can be several reasons for buffer creation to fail. "
                         "It "
                         "could be because device could not allocate enough memory "
                         "for this "
                         "buffer. The pointer could be null and either "
                         "CL_MEM_USE_HOST_PTR "
                         "or CL_MEM_COPY_HOST_PTR are passed into the flags "
                         "parameter. In "
                         "this case we passed zero(0) as the size of the buffer. We "
                         "intentionally caused this error so we will not be exiting "
                         "the "
                         "program.\n"
                      << std::endl;
        }
    }

    size_t size = elements * sizeof(int);
    std::cout << "Creating Buffers..." << std::endl;
    OCL_CHECK(err, cl::Buffer buffer_a(context, CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY, size, A.data(), &err));
    OCL_CHECK(err, cl::Buffer buffer_b(context, CL_MEM_USE_HOST_PTR | CL_MEM_READ_ONLY, size, B.data(), &err));
    OCL_CHECK(err, cl::Buffer buffer_result(context, CL_MEM_USE_HOST_PTR | CL_MEM_WRITE_ONLY, size, C.data(), &err));

    // Set the Kernel Arguments
    OCL_CHECK(err, err = kernel.setArg(0, buffer_result));
    OCL_CHECK(err, err = kernel.setArg(1, buffer_a));
    OCL_CHECK(err, err = kernel.setArg(2, buffer_b));
    OCL_CHECK(err, err = kernel.setArg(3, elements));

    // Copy input data to device global memory
    std::cout << "Copying data..." << std::endl;
    OCL_CHECK(err, err = q.enqueueMigrateMemObjects({buffer_a, buffer_b}, 0 /*0 means from host*/));

    // Launch the Kernel
    std::cout << "Launching Kernel..." << std::endl;
    OCL_CHECK(err, err = q.enqueueTask(kernel));

    // Copy Result from Device Global Memory to Host Local Memory
    std::cout << "Getting Results..." << std::endl;
    OCL_CHECK(err, err = q.enqueueMigrateMemObjects({buffer_result}, CL_MIGRATE_MEM_OBJECT_HOST));
    OCL_CHECK(err, err = q.finish());
    // OPENCL HOST CODE AREA ENDS

    bool match = true;
    for (int i = 0; i < elements; i++) {
        int host_result = A[i] + B[i];
        if (C[i] != host_result) {
            std::cout << "Error: Result mismatch" << std::endl;
            std::cout << "i = " << i << " CPU result = " << host_result << " Device result = " << C[i] << std::endl;
            match = false;
            break;
        }
    }

    std::cout << "TEST " << (match ? "PASSED" : "FAILED") << std::endl;
    return (match ? EXIT_SUCCESS : EXIT_FAILURE);
}
